// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net.Cache;
using System.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;

namespace System.Net
{
    public class WebClient : Component
    {
        private const int DefaultCopyBufferLength = 8192;
        private const int DefaultDownloadBufferLength = 65536;
        private const string DefaultUploadFileContentType = "application/octet-stream";
        private const string UploadFileContentType = "multipart/form-data";
        private const string UploadValuesContentType = "application/x-www-form-urlencoded";

        private Uri? _baseAddress;
        private ICredentials? _credentials;
        private WebHeaderCollection? _headers;
        private NameValueCollection? _requestParameters;
        private WebResponse? _webResponse;
        private WebRequest? _webRequest;
        private Encoding _encoding = Encoding.Default;
        private string? _method;
        private long _contentLength = -1;
        private bool _initWebClientAsync;
        private bool _canceled;
        private ProgressData? _progress;
        private IWebProxy? _proxy;
        private bool _proxySet;
        private int _callNesting; // > 0 if we're in a Read/Write call
        private AsyncOperation? _asyncOp;

        private SendOrPostCallback? _downloadDataOperationCompleted;
        private SendOrPostCallback? _openReadOperationCompleted;
        private SendOrPostCallback? _openWriteOperationCompleted;
        private SendOrPostCallback? _downloadStringOperationCompleted;
        private SendOrPostCallback? _downloadFileOperationCompleted;
        private SendOrPostCallback? _uploadStringOperationCompleted;
        private SendOrPostCallback? _uploadDataOperationCompleted;
        private SendOrPostCallback? _uploadFileOperationCompleted;
        private SendOrPostCallback? _uploadValuesOperationCompleted;
        private SendOrPostCallback? _reportDownloadProgressChanged;
        private SendOrPostCallback? _reportUploadProgressChanged;

        [Obsolete(Obsoletions.WebRequestMessage, DiagnosticId = Obsoletions.WebRequestDiagId, UrlFormat = Obsoletions.SharedUrlFormat)]
        public WebClient()
        {
            // We don't know if derived types need finalizing, but WebClient doesn't.
            if (GetType() == typeof(WebClient))
            {
                GC.SuppressFinalize(this);
            }
        }

        public event DownloadStringCompletedEventHandler? DownloadStringCompleted;
        public event DownloadDataCompletedEventHandler? DownloadDataCompleted;
        public event AsyncCompletedEventHandler? DownloadFileCompleted;
        public event UploadStringCompletedEventHandler? UploadStringCompleted;
        public event UploadDataCompletedEventHandler? UploadDataCompleted;
        public event UploadFileCompletedEventHandler? UploadFileCompleted;
        public event UploadValuesCompletedEventHandler? UploadValuesCompleted;
        public event OpenReadCompletedEventHandler? OpenReadCompleted;
        public event OpenWriteCompletedEventHandler? OpenWriteCompleted;
        public event DownloadProgressChangedEventHandler? DownloadProgressChanged;
        public event UploadProgressChangedEventHandler? UploadProgressChanged;

        protected virtual void OnDownloadStringCompleted(DownloadStringCompletedEventArgs e) => DownloadStringCompleted?.Invoke(this, e);
        protected virtual void OnDownloadDataCompleted(DownloadDataCompletedEventArgs e) => DownloadDataCompleted?.Invoke(this, e);
        protected virtual void OnDownloadFileCompleted(AsyncCompletedEventArgs e) => DownloadFileCompleted?.Invoke(this, e);
        protected virtual void OnDownloadProgressChanged(DownloadProgressChangedEventArgs e) => DownloadProgressChanged?.Invoke(this, e);
        protected virtual void OnUploadStringCompleted(UploadStringCompletedEventArgs e) => UploadStringCompleted?.Invoke(this, e);
        protected virtual void OnUploadDataCompleted(UploadDataCompletedEventArgs e) => UploadDataCompleted?.Invoke(this, e);
        protected virtual void OnUploadFileCompleted(UploadFileCompletedEventArgs e) => UploadFileCompleted?.Invoke(this, e);
        protected virtual void OnUploadValuesCompleted(UploadValuesCompletedEventArgs e) => UploadValuesCompleted?.Invoke(this, e);
        protected virtual void OnUploadProgressChanged(UploadProgressChangedEventArgs e) => UploadProgressChanged?.Invoke(this, e);
        protected virtual void OnOpenReadCompleted(OpenReadCompletedEventArgs e) => OpenReadCompleted?.Invoke(this, e);
        protected virtual void OnOpenWriteCompleted(OpenWriteCompletedEventArgs e) => OpenWriteCompleted?.Invoke(this, e);

        private void StartOperation()
        {
            if (Interlocked.Increment(ref _callNesting) > 1)
            {
                EndOperation();
                throw new NotSupportedException(SR.net_webclient_no_concurrent_io_allowed);
            }

            _contentLength = -1;
            _webResponse = null;
            _webRequest = null;
            _method = null;
            _canceled = false;

            _progress?.Reset();
        }

        private AsyncOperation StartAsyncOperation(object? userToken)
        {
            if (!_initWebClientAsync)
            {
                // Set up the async delegates

                _openReadOperationCompleted = arg => OnOpenReadCompleted((OpenReadCompletedEventArgs)arg!);
                _openWriteOperationCompleted = arg => OnOpenWriteCompleted((OpenWriteCompletedEventArgs)arg!);

                _downloadStringOperationCompleted = arg => OnDownloadStringCompleted((DownloadStringCompletedEventArgs)arg!);
                _downloadDataOperationCompleted = arg => OnDownloadDataCompleted((DownloadDataCompletedEventArgs)arg!);
                _downloadFileOperationCompleted = arg => OnDownloadFileCompleted((AsyncCompletedEventArgs)arg!);

                _uploadStringOperationCompleted = arg => OnUploadStringCompleted((UploadStringCompletedEventArgs)arg!);
                _uploadDataOperationCompleted = arg => OnUploadDataCompleted((UploadDataCompletedEventArgs)arg!);
                _uploadFileOperationCompleted = arg => OnUploadFileCompleted((UploadFileCompletedEventArgs)arg!);
                _uploadValuesOperationCompleted = arg => OnUploadValuesCompleted((UploadValuesCompletedEventArgs)arg!);

                _reportDownloadProgressChanged = arg => OnDownloadProgressChanged((DownloadProgressChangedEventArgs)arg!);
                _reportUploadProgressChanged = arg => OnUploadProgressChanged((UploadProgressChangedEventArgs)arg!);

                _progress = new ProgressData();
                _initWebClientAsync = true;
            }

            AsyncOperation asyncOp = AsyncOperationManager.CreateOperation(userToken);

            StartOperation();
            _asyncOp = asyncOp;

            return asyncOp;
        }

        private void EndOperation() => Interlocked.Decrement(ref _callNesting);

        public Encoding Encoding
        {
            get { return _encoding; }
            set
            {
                ArgumentNullException.ThrowIfNull(value);
                _encoding = value;
            }
        }

        [AllowNull]
        public string BaseAddress
        {
            get { return _baseAddress != null ? _baseAddress.ToString() : string.Empty; }
            set
            {
                if (string.IsNullOrEmpty(value))
                {
                    _baseAddress = null;
                }
                else
                {
                    try
                    {
                        _baseAddress = new Uri(value);
                    }
                    catch (UriFormatException e)
                    {
                        throw new ArgumentException(SR.net_webclient_invalid_baseaddress, nameof(value), e);
                    }
                }
            }
        }

        public ICredentials? Credentials
        {
            get { return _credentials; }
            set { _credentials = value; }
        }

        public bool UseDefaultCredentials
        {
            get { return _credentials == CredentialCache.DefaultCredentials; }
            set { _credentials = value ? CredentialCache.DefaultCredentials : null; }
        }

        [AllowNull]
        public WebHeaderCollection Headers
        {
            get { return _headers ??= new WebHeaderCollection(); }
            set { _headers = value; }
        }

        [AllowNull]
        public NameValueCollection QueryString
        {
            get { return _requestParameters ??= new NameValueCollection(); }
            set { _requestParameters = value; }
        }

        public WebHeaderCollection? ResponseHeaders => _webResponse?.Headers;

        public IWebProxy? Proxy
        {
            get { return _proxySet ? _proxy : WebRequest.DefaultWebProxy; }
            set
            {
                _proxy = value;
                _proxySet = true;
            }
        }

        public RequestCachePolicy? CachePolicy { get; set; }

        public bool IsBusy => Volatile.Read(ref _callNesting) > 0;

        protected virtual WebRequest GetWebRequest(Uri address)
        {
#pragma warning disable SYSLIB0014 // WebRequest, HttpWebRequest, ServicePoint, and WebClient are obsolete. Use HttpClient instead.
            WebRequest request = WebRequest.Create(address);
#pragma warning restore SYSLIB0014

            CopyHeadersTo(request);

            if (Credentials != null)
            {
                request.Credentials = Credentials;
            }

            if (_method != null)
            {
                request.Method = _method;
            }

            if (_contentLength != -1)
            {
                request.ContentLength = _contentLength;
            }

            if (_proxySet)
            {
                request.Proxy = _proxy;
            }

            if (CachePolicy != null)
            {
                request.CachePolicy = CachePolicy;
            }

            return request;
        }

        protected virtual WebResponse GetWebResponse(WebRequest request)
        {
            WebResponse response = request.GetResponse();
            _webResponse = response;
            return response;
        }

        protected virtual WebResponse GetWebResponse(WebRequest request, IAsyncResult result)
        {
            WebResponse response = request.EndGetResponse(result);
            _webResponse = response;
            return response;
        }

        private async Task<WebResponse> GetWebResponseTaskAsync(WebRequest request)
        {
            // We would like to simply await request.GetResponseAsync(), but WebClient exposes
            // a protected member GetWebResponse(WebRequest, IAsyncResult) that derived instances expect to
            // be used to get the response, and it needs to be passed the IAsyncResult that was returned
            // from WebRequest.BeginGetResponse.
            var awaitable = new BeginEndAwaitableAdapter();
            request.BeginGetResponse(BeginEndAwaitableAdapter.Callback, awaitable);
            return GetWebResponse(request, await awaitable);
        }

        public byte[] DownloadData(string address) =>
            DownloadData(GetUri(address));

        public byte[] DownloadData(Uri address)
        {
            ArgumentNullException.ThrowIfNull(address);

            StartOperation();
            try
            {
                WebRequest request;
                return DownloadDataInternal(address, out request);
            }
            finally
            {
                EndOperation();
            }
        }

        private byte[] DownloadDataInternal(Uri address, out WebRequest request)
        {
            WebRequest? tmpRequest = null;
            byte[] result;

            try
            {
                tmpRequest = _webRequest = GetWebRequest(GetUri(address));
                result = DownloadBits(tmpRequest, new ChunkedMemoryStream())!;
            }
            catch (Exception e) when (!(e is OutOfMemoryException))
            {
                AbortRequest(tmpRequest);
                if (e is WebException || e is SecurityException) throw;
                throw new WebException(SR.net_webclient, e);
            }

            request = tmpRequest;
            return result;
        }

        public void DownloadFile(string address, string fileName) =>
            DownloadFile(GetUri(address), fileName);

        public void DownloadFile(Uri address, string fileName)
        {
            ArgumentNullException.ThrowIfNull(address);
            ArgumentNullException.ThrowIfNull(fileName);

            WebRequest? request = null;
            FileStream? fs = null;
            bool succeeded = false;
            StartOperation();
            try
            {
                fs = new FileStream(fileName, FileMode.Create, FileAccess.Write);
                request = _webRequest = GetWebRequest(GetUri(address));
                DownloadBits(request, fs);
                succeeded = true;
            }
            catch (Exception e) when (!(e is OutOfMemoryException))
            {
                AbortRequest(request);
                if (e is WebException || e is SecurityException) throw;
                throw new WebException(SR.net_webclient, e);
            }
            finally
            {
                if (fs != null)
                {
                    fs.Close();
                    if (!succeeded)
                    {
                        File.Delete(fileName);
                    }
                }
                EndOperation();
            }
        }

        public Stream OpenRead(string address) =>
            OpenRead(GetUri(address));

        public Stream OpenRead(Uri address)
        {
            ArgumentNullException.ThrowIfNull(address);

            WebRequest? request = null;
            StartOperation();
            try
            {
                request = _webRequest = GetWebRequest(GetUri(address));
                WebResponse response = _webResponse = GetWebResponse(request);
                return response.GetResponseStream();
            }
            catch (Exception e) when (!(e is OutOfMemoryException))
            {
                AbortRequest(request);
                if (e is WebException || e is SecurityException) throw;
                throw new WebException(SR.net_webclient, e);
            }
            finally
            {
                EndOperation();
            }
        }

        public Stream OpenWrite(string address) =>
            OpenWrite(GetUri(address), null);

        public Stream OpenWrite(Uri address) =>
            OpenWrite(address, null);

        public Stream OpenWrite(string address, string? method) =>
            OpenWrite(GetUri(address), method);

        public Stream OpenWrite(Uri address, string? method)
        {
            ArgumentNullException.ThrowIfNull(address);

            method ??= MapToDefaultMethod(address);

            WebRequest? request = null;
            StartOperation();
            try
            {
                _method = method;
                request = _webRequest = GetWebRequest(GetUri(address));
                return new WebClientWriteStream(
                    request.GetRequestStream(),
                    request,
                    this);
            }
            catch (Exception e) when (!(e is OutOfMemoryException))
            {
                AbortRequest(request);
                if (e is WebException || e is SecurityException) throw;
                throw new WebException(SR.net_webclient, e);
            }
            finally
            {
                EndOperation();
            }
        }

        public byte[] UploadData(string address, byte[] data) =>
            UploadData(GetUri(address), null, data);

        public byte[] UploadData(Uri address, byte[] data) =>
            UploadData(address, null, data);

        public byte[] UploadData(string address, string? method, byte[] data) =>
            UploadData(GetUri(address), method, data);

        public byte[] UploadData(Uri address, string? method, byte[] data)
        {
            ArgumentNullException.ThrowIfNull(address);
            ArgumentNullException.ThrowIfNull(data);

            method ??= MapToDefaultMethod(address);

            StartOperation();
            try
            {
                WebRequest request;
                return UploadDataInternal(address, method, data, out request);
            }
            finally
            {
                EndOperation();
            }
        }

        private byte[] UploadDataInternal(Uri address, string method, byte[] data, out WebRequest request)
        {
            WebRequest? tmpRequest = null;
            byte[] result;

            try
            {
                _method = method;
                _contentLength = data.Length;
                tmpRequest = _webRequest = GetWebRequest(GetUri(address));
                result = UploadBits(tmpRequest, null, data, 0, null, null);
            }
            catch (Exception e) when (!(e is OutOfMemoryException))
            {
                AbortRequest(tmpRequest);
                if (e is WebException || e is SecurityException) throw;
                throw new WebException(SR.net_webclient, e);
            }

            request = tmpRequest;
            return result;
        }

        private void OpenFileInternal(
            bool needsHeaderAndBoundary, string fileName,
            out FileStream fs, out byte[] buffer, ref byte[]? formHeaderBytes, ref byte[]? boundaryBytes)
        {
            fileName = Path.GetFullPath(fileName);

            WebHeaderCollection headers = Headers;
            string? contentType = headers[HttpKnownHeaderNames.ContentType];

            if (contentType == null)
            {
                contentType = DefaultUploadFileContentType;
            }
            else if (contentType.StartsWith("multipart/", StringComparison.OrdinalIgnoreCase))
            {
                throw new WebException(SR.net_webclient_Multipart);
            }

            fs = new FileStream(fileName, FileMode.Open, FileAccess.Read);

            int buffSize = DefaultCopyBufferLength;
            _contentLength = -1;

            if (string.Equals(_method, "POST", StringComparison.Ordinal))
            {
                if (needsHeaderAndBoundary)
                {
                    string boundary = $"---------------------{DateTime.Now.Ticks:x}";

                    headers[HttpKnownHeaderNames.ContentType] = UploadFileContentType + "; boundary=" + boundary;

                    string formHeader =
                        "--" + boundary + "\r\n" +
                        "Content-Disposition: form-data; name=\"file\"; filename=\"" + Path.GetFileName(fileName) + "\"\r\n" + // TODO: Should the filename path be encoded?
                        "Content-Type: " + contentType + "\r\n" +
                        "\r\n";
                    formHeaderBytes = Encoding.UTF8.GetBytes(formHeader);

                    boundaryBytes = new byte["\r\n--".Length + boundary.Length + "--\r\n".Length];
                    "\r\n--"u8.CopyTo(boundaryBytes);
                    "--\r\n"u8.CopyTo(boundaryBytes.AsSpan("\r\n--".Length + boundary.Length));
                    OperationStatus conversionStatus = Ascii.FromUtf16(boundary, boundaryBytes.AsSpan("\r\n--".Length), out _);
                    Debug.Assert(conversionStatus == OperationStatus.Done);
                }
                else
                {
                    formHeaderBytes = Array.Empty<byte>();
                    boundaryBytes = Array.Empty<byte>();
                }

                if (fs.CanSeek)
                {
                    _contentLength = fs.Length + formHeaderBytes.Length + boundaryBytes.Length;
                    buffSize = (int)Math.Min(DefaultCopyBufferLength, fs.Length);
                }
            }
            else
            {
                headers[HttpKnownHeaderNames.ContentType] = contentType;

                formHeaderBytes = null;
                boundaryBytes = null;

                if (fs.CanSeek)
                {
                    _contentLength = fs.Length;
                    buffSize = (int)Math.Min(DefaultCopyBufferLength, fs.Length);
                }
            }

            buffer = new byte[buffSize];
        }

        public byte[] UploadFile(string address, string fileName) =>
            UploadFile(GetUri(address), fileName);

        public byte[] UploadFile(Uri address, string fileName) =>
            UploadFile(address, null, fileName);

        public byte[] UploadFile(string address, string? method, string fileName) =>
            UploadFile(GetUri(address), method, fileName);

        public byte[] UploadFile(Uri address, string? method, string fileName)
        {
            ArgumentNullException.ThrowIfNull(address);
            ArgumentNullException.ThrowIfNull(fileName);

            method ??= MapToDefaultMethod(address);

            FileStream? fs = null;
            WebRequest? request = null;
            StartOperation();
            try
            {
                _method = method;
                byte[]? formHeaderBytes = null, boundaryBytes = null, buffer;
                Uri uri = GetUri(address);
                bool needsHeaderAndBoundary = (uri.Scheme != Uri.UriSchemeFile);
                OpenFileInternal(needsHeaderAndBoundary, fileName, out fs, out buffer, ref formHeaderBytes, ref boundaryBytes);
                request = _webRequest = GetWebRequest(uri);
                return UploadBits(request, fs, buffer, 0, formHeaderBytes, boundaryBytes);
            }
            catch (Exception e)
            {
                fs?.Close();
                if (e is OutOfMemoryException) throw;
                AbortRequest(request);
                if (e is WebException || e is SecurityException) throw;
                throw new WebException(SR.net_webclient, e);
            }
            finally
            {
                EndOperation();
            }
        }

        private byte[] GetValuesToUpload(NameValueCollection data)
        {
            WebHeaderCollection headers = Headers;

            string? contentType = headers[HttpKnownHeaderNames.ContentType];
            if (contentType != null && !string.Equals(contentType, UploadValuesContentType, StringComparison.OrdinalIgnoreCase))
            {
                throw new WebException(SR.net_webclient_ContentType);
            }

            headers[HttpKnownHeaderNames.ContentType] = UploadValuesContentType;

            string delimiter = string.Empty;
            var values = new StringBuilder();
            foreach (string? name in data.AllKeys)
            {
                values.Append(delimiter);
                values.Append(UrlEncode(name));
                values.Append('=');
                values.Append(UrlEncode(data[name]));
                delimiter = "&";
            }

            byte[] buffer = Encoding.ASCII.GetBytes(values.ToString());
            _contentLength = buffer.Length;
            return buffer;
        }

        public byte[] UploadValues(string address, NameValueCollection data) =>
            UploadValues(GetUri(address), null, data);

        public byte[] UploadValues(Uri address, NameValueCollection data) =>
            UploadValues(address, null, data);

        public byte[] UploadValues(string address, string? method, NameValueCollection data) =>
            UploadValues(GetUri(address), method, data);

        public byte[] UploadValues(Uri address, string? method, NameValueCollection data)
        {
            ArgumentNullException.ThrowIfNull(address);
            ArgumentNullException.ThrowIfNull(data);

            method ??= MapToDefaultMethod(address);

            WebRequest? request = null;
            StartOperation();
            try
            {
                byte[] buffer = GetValuesToUpload(data);
                _method = method;
                request = _webRequest = GetWebRequest(GetUri(address));
                return UploadBits(request, null, buffer, 0, null, null);
            }
            catch (Exception e) when (!(e is OutOfMemoryException))
            {
                AbortRequest(request);
                if (e is WebException || e is SecurityException) throw;
                throw new WebException(SR.net_webclient, e);
            }
            finally
            {
                EndOperation();
            }
        }

        public string UploadString(string address, string data) =>
            UploadString(GetUri(address), null, data);

        public string UploadString(Uri address, string data) =>
            UploadString(address, null, data);

        public string UploadString(string address, string? method, string data) =>
            UploadString(GetUri(address), method, data);

        public string UploadString(Uri address, string? method, string data)
        {
            ArgumentNullException.ThrowIfNull(address);
            ArgumentNullException.ThrowIfNull(data);

            method ??= MapToDefaultMethod(address);

            StartOperation();
            try
            {
                WebRequest request;
                byte[] requestData = Encoding.GetBytes(data);
                byte[] responseData = UploadDataInternal(address, method, requestData, out request);
                return GetStringUsingEncoding(request, responseData);
            }
            finally
            {
                EndOperation();
            }
        }

        public string DownloadString(string address) =>
            DownloadString(GetUri(address));

        public string DownloadString(Uri address)
        {
            ArgumentNullException.ThrowIfNull(address);

            StartOperation();
            try
            {
                WebRequest request;
                byte[] data = DownloadDataInternal(address, out request);
                return GetStringUsingEncoding(request, data);
            }
            finally
            {
                EndOperation();
            }
        }

        private static void AbortRequest(WebRequest? request)
        {
            try { request?.Abort(); }
            catch (Exception exception) when (!(exception is OutOfMemoryException)) { }
        }

        private void CopyHeadersTo(WebRequest request)
        {
            if (_headers == null)
            {
                return;
            }

            var hwr = request as HttpWebRequest;
            if (hwr == null)
            {
                return;
            }

            string? accept = _headers[HttpKnownHeaderNames.Accept];
            string? connection = _headers[HttpKnownHeaderNames.Connection];
            string? contentType = _headers[HttpKnownHeaderNames.ContentType];
            string? expect = _headers[HttpKnownHeaderNames.Expect];
            string? referrer = _headers[HttpKnownHeaderNames.Referer];
            string? userAgent = _headers[HttpKnownHeaderNames.UserAgent];
            string? host = _headers[HttpKnownHeaderNames.Host];

            _headers.Remove(HttpKnownHeaderNames.Accept);
            _headers.Remove(HttpKnownHeaderNames.Connection);
            _headers.Remove(HttpKnownHeaderNames.ContentType);
            _headers.Remove(HttpKnownHeaderNames.Expect);
            _headers.Remove(HttpKnownHeaderNames.Referer);
            _headers.Remove(HttpKnownHeaderNames.UserAgent);
            _headers.Remove(HttpKnownHeaderNames.Host);

            request.Headers = _headers;

            if (!string.IsNullOrEmpty(accept))
            {
                hwr.Accept = accept;
            }

            if (!string.IsNullOrEmpty(connection))
            {
                hwr.Connection = connection;
            }

            if (!string.IsNullOrEmpty(contentType))
            {
                hwr.ContentType = contentType;
            }

            if (!string.IsNullOrEmpty(expect))
            {
                hwr.Expect = expect;
            }

            if (!string.IsNullOrEmpty(referrer))
            {
                hwr.Referer = referrer;
            }

            if (!string.IsNullOrEmpty(userAgent))
            {
                hwr.UserAgent = userAgent;
            }

            if (!string.IsNullOrEmpty(host))
            {
                hwr.Host = host;
            }
        }

        private Uri GetUri(string address)
        {
            ArgumentNullException.ThrowIfNull(address);

            Uri? uri;
            if (_baseAddress != null)
            {
                if (!Uri.TryCreate(_baseAddress, address, out uri))
                {
                    return new Uri(Path.GetFullPath(address));
                }
            }
            else if (!Uri.TryCreate(address, UriKind.Absolute, out uri))
            {
                return new Uri(Path.GetFullPath(address));
            }

            return GetUri(uri);
        }

        private Uri GetUri(Uri address)
        {
            ArgumentNullException.ThrowIfNull(address);

            Uri? uri = address;

            if (!address.IsAbsoluteUri && _baseAddress != null && !Uri.TryCreate(_baseAddress, address, out uri))
            {
                return address;
            }

            if (string.IsNullOrEmpty(uri.Query) && _requestParameters != null)
            {
                var sb = new StringBuilder();

                string delimiter = string.Empty;
                for (int i = 0; i < _requestParameters.Count; ++i)
                {
                    sb.Append(delimiter).Append(_requestParameters.AllKeys[i]).Append('=').Append(_requestParameters[i]);
                    delimiter = "&";
                }

                uri = new UriBuilder(uri) { Query = sb.ToString() }.Uri;
            }

            return uri;
        }

        private byte[]? DownloadBits(WebRequest request, Stream writeStream)
        {
            try
            {
                WebResponse response = _webResponse = GetWebResponse(request);

                long contentLength = response.ContentLength;
                byte[] copyBuffer = new byte[contentLength == -1 || contentLength > DefaultDownloadBufferLength ? DefaultDownloadBufferLength : contentLength];

                if (writeStream is ChunkedMemoryStream)
                {
                    if (contentLength > int.MaxValue)
                    {
                        throw new WebException(SR.net_webstatus_MessageLengthLimitExceeded, WebExceptionStatus.MessageLengthLimitExceeded);
                    }
                    writeStream.SetLength(copyBuffer.Length);
                }

                using (Stream readStream = response.GetResponseStream())
                {
                    if (readStream != null)
                    {
                        int bytesRead;
                        while ((bytesRead = readStream.Read(copyBuffer, 0, copyBuffer.Length)) != 0)
                        {
                            writeStream.Write(copyBuffer, 0, bytesRead);
                        }
                    }
                }

                return (writeStream as ChunkedMemoryStream)?.ToArray();
            }
            catch (Exception e) when (!(e is OutOfMemoryException))
            {
                writeStream?.Close();
                AbortRequest(request);
                if (e is WebException || e is SecurityException) throw;
                throw new WebException(SR.net_webclient, e);
            }
        }

        private async void DownloadBitsAsync(
            WebRequest request, Stream writeStream,
            AsyncOperation asyncOp, Action<byte[]?, Exception?, AsyncOperation> completionDelegate)
        {
            Debug.Assert(_progress != null, "ProgressData should have been initialized");
            Debug.Assert(asyncOp != null);

            Exception? exception = null;
            try
            {
                WebResponse response = _webResponse = await GetWebResponseTaskAsync(request).ConfigureAwait(false);

                long contentLength = response.ContentLength;
                byte[] copyBuffer = new byte[contentLength == -1 || contentLength > DefaultDownloadBufferLength ? DefaultDownloadBufferLength : contentLength];

                if (writeStream is ChunkedMemoryStream)
                {
                    if (contentLength > int.MaxValue)
                    {
                        throw new WebException(SR.net_webstatus_MessageLengthLimitExceeded, WebExceptionStatus.MessageLengthLimitExceeded);
                    }
                    writeStream.SetLength(copyBuffer.Length);
                }

                if (contentLength >= 0)
                {
                    _progress.TotalBytesToReceive = contentLength;
                }

                using (writeStream)
                using (Stream readStream = response.GetResponseStream())
                {
                    if (readStream != null)
                    {
                        while (true)
                        {
                            int bytesRead = await readStream.ReadAsync(new Memory<byte>(copyBuffer)).ConfigureAwait(false);
                            if (bytesRead == 0)
                            {
                                break;
                            }

                            _progress.BytesReceived += bytesRead;
                            if (_progress.BytesReceived != _progress.TotalBytesToReceive)
                            {
                                PostProgressChanged(asyncOp, _progress);
                            }

                            await writeStream.WriteAsync(new ReadOnlyMemory<byte>(copyBuffer, 0, bytesRead)).ConfigureAwait(false);
                        }
                    }

                    if (_progress.TotalBytesToReceive < 0)
                    {
                        _progress.TotalBytesToReceive = _progress.BytesReceived;
                    }
                    PostProgressChanged(asyncOp, _progress);
                }

                completionDelegate((writeStream as ChunkedMemoryStream)?.ToArray(), null, asyncOp);
            }
            catch (Exception e) when (!(e is OutOfMemoryException))
            {
                exception = GetExceptionToPropagate(e);
                AbortRequest(request);
                writeStream?.Close();
            }
            finally
            {
                if (exception != null)
                {
                    completionDelegate(null, exception, asyncOp);
                }
            }
        }

        private byte[] UploadBits(
            WebRequest request, FileStream? readStream, byte[] buffer, int chunkSize,
            byte[]? header, byte[]? footer)
        {
            try
            {
                if (request.RequestUri.Scheme == Uri.UriSchemeFile)
                {
                    header = footer = null;
                }

                using (Stream writeStream = request.GetRequestStream())
                {
                    if (header != null)
                    {
                        writeStream.Write(header, 0, header.Length);
                    }

                    if (readStream != null)
                    {
                        using (readStream)
                        {
                            while (true)
                            {
                                int bytesRead = readStream.Read(buffer, 0, buffer.Length);
                                if (bytesRead <= 0)
                                    break;
                                writeStream.Write(buffer, 0, bytesRead);
                            }
                        }
                    }
                    else
                    {
                        for (int pos = 0; pos < buffer.Length;)
                        {
                            int toWrite = buffer.Length - pos;
                            if (chunkSize != 0 && toWrite > chunkSize)
                            {
                                toWrite = chunkSize;
                            }
                            writeStream.Write(buffer, pos, toWrite);
                            pos += toWrite;
                        }
                    }

                    if (footer != null)
                    {
                        writeStream.Write(footer, 0, footer.Length);
                    }
                }

                return DownloadBits(request, new ChunkedMemoryStream())!;
            }
            catch (Exception e) when (!(e is OutOfMemoryException))
            {
                AbortRequest(request);
                if (e is WebException || e is SecurityException) throw;
                throw new WebException(SR.net_webclient, e);
            }
        }

        private async void UploadBitsAsync(
            WebRequest request, FileStream? readStream, byte[] buffer, int chunkSize,
            byte[]? header, byte[]? footer,
            AsyncOperation asyncOp, Action<byte[]?, Exception?, AsyncOperation> completionDelegate)
        {
            Debug.Assert(asyncOp != null);
            Debug.Assert(_progress != null, "ProgressData should have been initialized");
            _progress.HasUploadPhase = true;

            Exception? exception = null;
            try
            {
                if (request.RequestUri.Scheme == Uri.UriSchemeFile)
                {
                    header = footer = null;
                }

                using (Stream writeStream = await request.GetRequestStreamAsync().ConfigureAwait(false))
                {
                    if (header != null)
                    {
                        await writeStream.WriteAsync(new ReadOnlyMemory<byte>(header)).ConfigureAwait(false);
                        _progress.BytesSent += header.Length;
                        PostProgressChanged(asyncOp, _progress);
                    }

                    if (readStream != null)
                    {
                        using (readStream)
                        {
                            while (true)
                            {
                                int bytesRead = await readStream.ReadAsync(new Memory<byte>(buffer)).ConfigureAwait(false);
                                if (bytesRead <= 0) break;
                                await writeStream.WriteAsync(new ReadOnlyMemory<byte>(buffer, 0, bytesRead)).ConfigureAwait(false);

                                _progress.BytesSent += bytesRead;
                                PostProgressChanged(asyncOp, _progress);
                            }
                        }
                    }
                    else
                    {
                        for (int pos = 0; pos < buffer.Length;)
                        {
                            int toWrite = buffer.Length - pos;
                            if (chunkSize != 0 && toWrite > chunkSize)
                            {
                                toWrite = chunkSize;
                            }
                            await writeStream.WriteAsync(new ReadOnlyMemory<byte>(buffer, pos, toWrite)).ConfigureAwait(false);
                            pos += toWrite;
                            _progress.BytesSent += toWrite;
                            PostProgressChanged(asyncOp, _progress);
                        }
                    }

                    if (footer != null)
                    {
                        await writeStream.WriteAsync(new ReadOnlyMemory<byte>(footer)).ConfigureAwait(false);
                        _progress.BytesSent += footer.Length;
                        PostProgressChanged(asyncOp, _progress);
                    }
                }

                DownloadBitsAsync(request, new ChunkedMemoryStream(), asyncOp, completionDelegate);
            }
            catch (Exception e) when (!(e is OutOfMemoryException))
            {
                exception = GetExceptionToPropagate(e);
                AbortRequest(request);
            }
            finally
            {
                if (exception != null)
                {
                    completionDelegate(null, exception, asyncOp);
                }
            }
        }

        private static readonly char[] s_parseContentTypeSeparators = new char[] { ';', '=', ' ' };
        private static readonly Encoding[] s_knownEncodings = { Encoding.UTF8, Encoding.UTF32, Encoding.Unicode, Encoding.BigEndianUnicode };

        private string GetStringUsingEncoding(WebRequest request, byte[] data)
        {
            Encoding? enc = null;
            int bomLengthInData = -1;

            // Figure out encoding by first checking for encoding string in Content-Type HTTP header
            // This can throw NotImplementedException if the derived class of WebRequest doesn't support it.
            string? contentType;
            try
            {
                contentType = request.ContentType;
            }
            catch (Exception e) when (e is NotImplementedException || e is NotSupportedException) // some types do this
            {
                contentType = null;
            }

            if (contentType != null)
            {
                contentType = contentType.ToLowerInvariant();
                string[] parsedList = contentType.Split(s_parseContentTypeSeparators);
                bool nextItem = false;
                foreach (string item in parsedList)
                {
                    if (item == "charset")
                    {
                        nextItem = true;
                    }
                    else if (nextItem)
                    {
                        try
                        {
                            enc = Encoding.GetEncoding(item);
                        }
                        catch (ArgumentException)
                        {
                            // Eat ArgumentException here.
                            // We'll assume that Content-Type encoding might have been garbled and wasn't present at all.
                            break;
                        }
                        // Unexpected exceptions are thrown back to caller
                    }
                }
            }

            // If no content encoding listed in the ContentType HTTP header, or no Content-Type header present, then
            // check for a byte-order-mark (BOM) in the data to figure out encoding.
            if (enc == null)
            {
                // UTF32 must be tested before Unicode because it's BOM is the same but longer.
                foreach (Encoding encoding in s_knownEncodings)
                {
                    ReadOnlySpan<byte> preamble = encoding.Preamble;
                    if (data.AsSpan().StartsWith(preamble))
                    {
                        enc = encoding;
                        bomLengthInData = preamble.Length;
                        break;
                    }
                }
            }

            // Do we have an encoding guess?  If not, use default.
            enc ??= Encoding;

            // Calculate BOM length based on encoding guess.  Then check for it in the data.
            if (bomLengthInData == -1)
            {
                ReadOnlySpan<byte> preamble = enc.Preamble;
                bomLengthInData = data.AsSpan().StartsWith(preamble) ? preamble.Length : 0;
            }

            // Convert byte array to string stripping off any BOM before calling Format().
            // This is required since Format() doesn't handle stripping off BOM.
            return enc.GetString(data, bomLengthInData, data.Length - bomLengthInData);
        }

        private string MapToDefaultMethod(Uri address)
        {
            Uri uri = !address.IsAbsoluteUri && _baseAddress != null ?
                new Uri(_baseAddress, address) :
                address;

            return string.Equals(uri.Scheme, Uri.UriSchemeFtp, StringComparison.Ordinal) ?
                "STOR" :
                "POST";
        }

        [return: NotNullIfNotNull(nameof(str))]
        private static string? UrlEncode(string? str) =>
            str is null ? null :
            HttpUtility.UrlEncode(str);

        private void InvokeOperationCompleted(AsyncOperation asyncOp, SendOrPostCallback callback, AsyncCompletedEventArgs eventArgs)
        {
            if (Interlocked.CompareExchange(ref _asyncOp, null, asyncOp) == asyncOp)
            {
                EndOperation();
                asyncOp.PostOperationCompleted(callback, eventArgs);
            }
        }

        public void OpenReadAsync(Uri address) =>
            OpenReadAsync(address, null);

        public void OpenReadAsync(Uri address, object? userToken)
        {
            ArgumentNullException.ThrowIfNull(address);

            AsyncOperation asyncOp = StartAsyncOperation(userToken);
            try
            {
                WebRequest request = _webRequest = GetWebRequest(GetUri(address));
                request.BeginGetResponse(iar =>
                {
                    Stream? stream = null;
                    Exception? exception = null;
                    try
                    {
                        WebResponse response = _webResponse = GetWebResponse(request, iar);
                        stream = response.GetResponseStream();
                    }
                    catch (Exception e) when (!(e is OutOfMemoryException))
                    {
                        exception = GetExceptionToPropagate(e);
                    }

                    InvokeOperationCompleted(asyncOp, _openReadOperationCompleted!, new OpenReadCompletedEventArgs(stream, exception, _canceled, asyncOp.UserSuppliedState));
                }, null);
            }
            catch (Exception e) when (!(e is OutOfMemoryException))
            {
                InvokeOperationCompleted(asyncOp, _openReadOperationCompleted!,
                    new OpenReadCompletedEventArgs(null, GetExceptionToPropagate(e), _canceled, asyncOp.UserSuppliedState));
            }
        }

        public void OpenWriteAsync(Uri address) =>
            OpenWriteAsync(address, null, null);

        public void OpenWriteAsync(Uri address, string? method) =>
            OpenWriteAsync(address, method, null);

        public void OpenWriteAsync(Uri address, string? method, object? userToken)
        {
            ArgumentNullException.ThrowIfNull(address);

            method ??= MapToDefaultMethod(address);

            AsyncOperation asyncOp = StartAsyncOperation(userToken);
            try
            {
                _method = method;
                WebRequest request = _webRequest = GetWebRequest(GetUri(address));
                request.BeginGetRequestStream(iar =>
                {
                    WebClientWriteStream? stream = null;
                    Exception? exception = null;

                    try
                    {
                        stream = new WebClientWriteStream(request.EndGetRequestStream(iar), request, this);
                    }
                    catch (Exception e) when (!(e is OutOfMemoryException))
                    {
                        exception = GetExceptionToPropagate(e);
                    }

                    InvokeOperationCompleted(asyncOp, _openWriteOperationCompleted!, new OpenWriteCompletedEventArgs(stream, exception, _canceled, asyncOp.UserSuppliedState));
                }, null);
            }
            catch (Exception e) when (!(e is OutOfMemoryException))
            {
                var eventArgs = new OpenWriteCompletedEventArgs(null, GetExceptionToPropagate(e), _canceled, asyncOp.UserSuppliedState);
                InvokeOperationCompleted(asyncOp, _openWriteOperationCompleted!, eventArgs);
            }
        }

        private void DownloadStringAsyncCallback(byte[]? returnBytes, Exception? exception, object state)
        {
            AsyncOperation asyncOp = (AsyncOperation)state;
            string? stringData = null;
            try
            {
                if (returnBytes != null)
                {
                    stringData = GetStringUsingEncoding(_webRequest!, returnBytes);
                }
            }
            catch (Exception e) when (!(e is OutOfMemoryException))
            {
                exception = GetExceptionToPropagate(e);
            }

            var eventArgs = new DownloadStringCompletedEventArgs(stringData, exception, _canceled, asyncOp.UserSuppliedState);
            InvokeOperationCompleted(asyncOp, _downloadStringOperationCompleted!, eventArgs);
        }


        public void DownloadStringAsync(Uri address) =>
            DownloadStringAsync(address, null);

        public void DownloadStringAsync(Uri address, object? userToken)
        {
            ArgumentNullException.ThrowIfNull(address);

            AsyncOperation asyncOp = StartAsyncOperation(userToken);
            try
            {
                WebRequest request = _webRequest = GetWebRequest(GetUri(address));
                DownloadBitsAsync(request, new ChunkedMemoryStream(), asyncOp, DownloadStringAsyncCallback);
            }
            catch (Exception e) when (!(e is OutOfMemoryException))
            {
                DownloadStringAsyncCallback(null, GetExceptionToPropagate(e), asyncOp);
            }
        }

        private void DownloadDataAsyncCallback(byte[]? returnBytes, Exception? exception, object state)
        {
            AsyncOperation asyncOp = (AsyncOperation)state;
            DownloadDataCompletedEventArgs eventArgs = new DownloadDataCompletedEventArgs(returnBytes, exception, _canceled, asyncOp.UserSuppliedState!);
            InvokeOperationCompleted(asyncOp, _downloadDataOperationCompleted!, eventArgs);
        }

        public void DownloadDataAsync(Uri address) =>
            DownloadDataAsync(address, null);

        public void DownloadDataAsync(Uri address, object? userToken)
        {
            ArgumentNullException.ThrowIfNull(address);

            AsyncOperation asyncOp = StartAsyncOperation(userToken);
            try
            {
                WebRequest request = _webRequest = GetWebRequest(GetUri(address));
                DownloadBitsAsync(request, new ChunkedMemoryStream(), asyncOp, DownloadDataAsyncCallback);
            }
            catch (Exception e) when (!(e is OutOfMemoryException))
            {
                DownloadDataAsyncCallback(null, GetExceptionToPropagate(e), asyncOp);
            }
        }

        private void DownloadFileAsyncCallback(byte[]? returnBytes, Exception? exception, object state)
        {
            AsyncOperation asyncOp = (AsyncOperation)state;
            AsyncCompletedEventArgs eventArgs = new AsyncCompletedEventArgs(exception, _canceled, asyncOp.UserSuppliedState);
            InvokeOperationCompleted(asyncOp, _downloadFileOperationCompleted!, eventArgs);
        }

        public void DownloadFileAsync(Uri address, string fileName) =>
            DownloadFileAsync(address, fileName, null);

        public void DownloadFileAsync(Uri address, string fileName, object? userToken)
        {
            ArgumentNullException.ThrowIfNull(address);
            ArgumentNullException.ThrowIfNull(fileName);

            FileStream? fs = null;
            AsyncOperation asyncOp = StartAsyncOperation(userToken);
            try
            {
                fs = new FileStream(fileName, FileMode.Create, FileAccess.Write);
                WebRequest request = _webRequest = GetWebRequest(GetUri(address));
                DownloadBitsAsync(request, fs, asyncOp, DownloadFileAsyncCallback);
            }
            catch (Exception e) when (!(e is OutOfMemoryException))
            {
                fs?.Close();
                DownloadFileAsyncCallback(null, GetExceptionToPropagate(e), asyncOp);
            }
        }

        public void UploadStringAsync(Uri address, string data) =>
            UploadStringAsync(address, null, data, null);

        public void UploadStringAsync(Uri address, string? method, string data) =>
            UploadStringAsync(address, method, data, null);

        public void UploadStringAsync(Uri address, string? method, string data, object? userToken)
        {
            ArgumentNullException.ThrowIfNull(address);
            ArgumentNullException.ThrowIfNull(data);

            method ??= MapToDefaultMethod(address);

            AsyncOperation asyncOp = StartAsyncOperation(userToken);
            try
            {
                byte[] requestData = Encoding.GetBytes(data);
                _method = method;
                _contentLength = requestData.Length;
                WebRequest request = _webRequest = GetWebRequest(GetUri(address));

                UploadBitsAsync(
                    request, null, requestData, 0, null, null, asyncOp,
                    (bytesResult, error, uploadAsyncOp) =>
                    {
                        string? stringResult = null;
                        if (error == null && bytesResult != null)
                        {
                            try
                            {
                                stringResult = GetStringUsingEncoding(_webRequest, bytesResult);
                            }
                            catch (Exception e) when (!(e is OutOfMemoryException))
                            {
                                error = GetExceptionToPropagate(e);
                            }
                        }

                        InvokeOperationCompleted(uploadAsyncOp, _uploadStringOperationCompleted!,
                            new UploadStringCompletedEventArgs(stringResult, error, _canceled, uploadAsyncOp.UserSuppliedState));
                    });
            }
            catch (Exception e) when (!(e is OutOfMemoryException))
            {
                var eventArgs = new UploadStringCompletedEventArgs(null, GetExceptionToPropagate(e), _canceled, asyncOp.UserSuppliedState);
                InvokeOperationCompleted(asyncOp, _uploadStringOperationCompleted!, eventArgs);
            }
        }

        public void UploadDataAsync(Uri address, byte[] data) =>
            UploadDataAsync(address, null, data, null);

        public void UploadDataAsync(Uri address, string? method, byte[] data) =>
            UploadDataAsync(address, method, data, null);

        public void UploadDataAsync(Uri address, string? method, byte[] data, object? userToken)
        {
            ArgumentNullException.ThrowIfNull(address);
            ArgumentNullException.ThrowIfNull(data);

            method ??= MapToDefaultMethod(address);

            AsyncOperation asyncOp = StartAsyncOperation(userToken);
            try
            {
                _method = method;
                _contentLength = data.Length;
                WebRequest request = _webRequest = GetWebRequest(GetUri(address));

                int chunkSize = 0;
                if (UploadProgressChanged != null)
                {
                    // If ProgressCallback is requested, we should send the buffer in chunks
                    chunkSize = (int)Math.Min((long)DefaultCopyBufferLength, data.Length);
                }

                UploadBitsAsync(
                    request, null, data, chunkSize, null, null, asyncOp,
                    (result, error, uploadAsyncOp) =>
                        InvokeOperationCompleted(asyncOp, _uploadDataOperationCompleted!, new UploadDataCompletedEventArgs(result, error, _canceled, uploadAsyncOp.UserSuppliedState)));
            }
            catch (Exception e) when (!(e is OutOfMemoryException))
            {
                var eventArgs = new UploadDataCompletedEventArgs(null, GetExceptionToPropagate(e), _canceled, asyncOp.UserSuppliedState);
                InvokeOperationCompleted(asyncOp, _uploadDataOperationCompleted!, eventArgs);
            }
        }

        public void UploadFileAsync(Uri address, string fileName) =>
            UploadFileAsync(address, null, fileName, null);

        public void UploadFileAsync(Uri address, string? method, string fileName) =>
            UploadFileAsync(address, method, fileName, null);

        public void UploadFileAsync(Uri address, string? method, string fileName, object? userToken)
        {
            ArgumentNullException.ThrowIfNull(address);
            ArgumentNullException.ThrowIfNull(fileName);

            method ??= MapToDefaultMethod(address);

            FileStream? fs = null;
            AsyncOperation asyncOp = StartAsyncOperation(userToken);
            try
            {
                _method = method;
                byte[]? formHeaderBytes = null, boundaryBytes = null, buffer = null;
                Uri uri = GetUri(address);
                bool needsHeaderAndBoundary = (uri.Scheme != Uri.UriSchemeFile);
                OpenFileInternal(needsHeaderAndBoundary, fileName, out fs, out buffer, ref formHeaderBytes, ref boundaryBytes);
                WebRequest request = _webRequest = GetWebRequest(uri);

                UploadBitsAsync(
                    request, fs, buffer, 0, formHeaderBytes, boundaryBytes, asyncOp,
                    (result, error, uploadAsyncOp) =>
                        InvokeOperationCompleted(asyncOp, _uploadFileOperationCompleted!, new UploadFileCompletedEventArgs(result, error, _canceled, uploadAsyncOp.UserSuppliedState)));
            }
            catch (Exception e) when (!(e is OutOfMemoryException))
            {
                fs?.Close();
                var eventArgs = new UploadFileCompletedEventArgs(null, GetExceptionToPropagate(e), _canceled, asyncOp.UserSuppliedState);
                InvokeOperationCompleted(asyncOp, _uploadFileOperationCompleted!, eventArgs);
            }
        }

        public void UploadValuesAsync(Uri address, NameValueCollection data) =>
            UploadValuesAsync(address, null, data, null);

        public void UploadValuesAsync(Uri address, string? method, NameValueCollection data) =>
            UploadValuesAsync(address, method, data, null);

        public void UploadValuesAsync(Uri address, string? method, NameValueCollection data, object? userToken)
        {
            ArgumentNullException.ThrowIfNull(address);
            ArgumentNullException.ThrowIfNull(data);

            method ??= MapToDefaultMethod(address);

            AsyncOperation asyncOp = StartAsyncOperation(userToken);
            try
            {
                byte[] buffer = GetValuesToUpload(data);
                _method = method;
                WebRequest request = _webRequest = GetWebRequest(GetUri(address));

                int chunkSize = 0;
                if (UploadProgressChanged != null)
                {
                    // If ProgressCallback is requested, we should send the buffer in chunks
                    chunkSize = (int)Math.Min((long)DefaultCopyBufferLength, buffer.Length);
                }

                UploadBitsAsync(
                    request, null, buffer, chunkSize, null, null, asyncOp,
                    (result, error, uploadAsyncOp) =>
                        InvokeOperationCompleted(asyncOp, _uploadValuesOperationCompleted!, new UploadValuesCompletedEventArgs(result, error, _canceled, uploadAsyncOp.UserSuppliedState)));
            }
            catch (Exception e) when (!(e is OutOfMemoryException))
            {
                var eventArgs = new UploadValuesCompletedEventArgs(null, GetExceptionToPropagate(e), _canceled, asyncOp.UserSuppliedState!);
                InvokeOperationCompleted(asyncOp, _uploadValuesOperationCompleted!, eventArgs);
            }
        }

        private static Exception GetExceptionToPropagate(Exception e) =>
            e is WebException || e is SecurityException ? e : new WebException(SR.net_webclient, e);

        public void CancelAsync()
        {
            WebRequest? request = _webRequest;
            _canceled = true;
            AbortRequest(request);
        }

        public Task<string> DownloadStringTaskAsync(string address) =>
            DownloadStringTaskAsync(GetUri(address));

        public Task<string> DownloadStringTaskAsync(Uri address)
        {
            // Create the task to be returned
            var tcs = new TaskCompletionSource<string>(address);

            DownloadStringCompletedEventHandler? handler = null;
            handler = (sender, e) => HandleCompletion(tcs, e, (args) => args.Result, handler, (webClient, completion) => webClient.DownloadStringCompleted -= completion);
            DownloadStringCompleted += handler;

            // Start the async operation.
            try { DownloadStringAsync(address, tcs); }
            catch
            {
                DownloadStringCompleted -= handler;
                throw;
            }

            // Return the task that represents the async operation
            return tcs.Task;
        }

        public Task<Stream> OpenReadTaskAsync(string address) =>
            OpenReadTaskAsync(GetUri(address));

        public Task<Stream> OpenReadTaskAsync(Uri address)
        {
            // Create the task to be returned
            var tcs = new TaskCompletionSource<Stream>(address);

            // Setup the callback event handler
            OpenReadCompletedEventHandler? handler = null;
            handler = (sender, e) => HandleCompletion(tcs, e, (args) => args.Result, handler, (webClient, completion) => webClient.OpenReadCompleted -= completion);
            OpenReadCompleted += handler;

            // Start the async operation.
            try { OpenReadAsync(address, tcs); }
            catch
            {
                OpenReadCompleted -= handler;
                throw;
            }

            // Return the task that represents the async operation
            return tcs.Task;
        }

        public Task<Stream> OpenWriteTaskAsync(string address) =>
            OpenWriteTaskAsync(GetUri(address), null);

        public Task<Stream> OpenWriteTaskAsync(Uri address) =>
            OpenWriteTaskAsync(address, null);

        public Task<Stream> OpenWriteTaskAsync(string address, string? method) =>
            OpenWriteTaskAsync(GetUri(address), method);

        public Task<Stream> OpenWriteTaskAsync(Uri address, string? method)
        {
            // Create the task to be returned
            var tcs = new TaskCompletionSource<Stream>(address);

            // Setup the callback event handler
            OpenWriteCompletedEventHandler? handler = null;
            handler = (sender, e) => HandleCompletion(tcs, e, (args) => args.Result, handler, (webClient, completion) => webClient.OpenWriteCompleted -= completion);
            OpenWriteCompleted += handler;

            // Start the async operation.
            try { OpenWriteAsync(address, method, tcs); }
            catch
            {
                OpenWriteCompleted -= handler;
                throw;
            }

            // Return the task that represents the async operation
            return tcs.Task;
        }

        public Task<string> UploadStringTaskAsync(string address, string data) =>
            UploadStringTaskAsync(address, null, data);

        public Task<string> UploadStringTaskAsync(Uri address, string data) =>
            UploadStringTaskAsync(address, null, data);

        public Task<string> UploadStringTaskAsync(string address, string? method, string data) =>
            UploadStringTaskAsync(GetUri(address), method, data);

        public Task<string> UploadStringTaskAsync(Uri address, string? method, string data)
        {
            // Create the task to be returned
            var tcs = new TaskCompletionSource<string>(address);

            // Setup the callback event handler
            UploadStringCompletedEventHandler? handler = null;
            handler = (sender, e) => HandleCompletion(tcs, e, (args) => args.Result, handler, (webClient, completion) => webClient.UploadStringCompleted -= completion);
            UploadStringCompleted += handler;

            // Start the async operation.
            try { UploadStringAsync(address, method, data, tcs); }
            catch
            {
                UploadStringCompleted -= handler;
                throw;
            }

            // Return the task that represents the async operation
            return tcs.Task;
        }

        public Task<byte[]> DownloadDataTaskAsync(string address) =>
            DownloadDataTaskAsync(GetUri(address));

        public Task<byte[]> DownloadDataTaskAsync(Uri address)
        {
            // Create the task to be returned
            var tcs = new TaskCompletionSource<byte[]>(address);

            // Setup the callback event handler
            DownloadDataCompletedEventHandler? handler = null;
            handler = (sender, e) => HandleCompletion(tcs, e, (args) => args.Result, handler, (webClient, completion) => webClient.DownloadDataCompleted -= completion);
            DownloadDataCompleted += handler;

            // Start the async operation.
            try { DownloadDataAsync(address, tcs); }
            catch
            {
                DownloadDataCompleted -= handler;
                throw;
            }

            // Return the task that represents the async operation
            return tcs.Task;
        }

        public Task DownloadFileTaskAsync(string address, string fileName) =>
            DownloadFileTaskAsync(GetUri(address), fileName);

        public Task DownloadFileTaskAsync(Uri address, string fileName)
        {
            // Create the task to be returned
            var tcs = new TaskCompletionSource<object?>(address);

            // Setup the callback event handler
            AsyncCompletedEventHandler? handler = null;
            handler = (sender, e) => HandleCompletion(tcs, e, (args) => null, handler, (webClient, completion) => webClient.DownloadFileCompleted -= completion);
            DownloadFileCompleted += handler;

            // Start the async operation.
            try { DownloadFileAsync(address, fileName, tcs); }
            catch
            {
                DownloadFileCompleted -= handler;
                throw;
            }

            // Return the task that represents the async operation
            return tcs.Task;
        }

        public Task<byte[]> UploadDataTaskAsync(string address, byte[] data) =>
            UploadDataTaskAsync(GetUri(address), null, data);

        public Task<byte[]> UploadDataTaskAsync(Uri address, byte[] data) =>
            UploadDataTaskAsync(address, null, data);

        public Task<byte[]> UploadDataTaskAsync(string address, string? method, byte[] data) =>
            UploadDataTaskAsync(GetUri(address), method, data);

        public Task<byte[]> UploadDataTaskAsync(Uri address, string? method, byte[] data)
        {
            // Create the task to be returned
            var tcs = new TaskCompletionSource<byte[]>(address);

            // Setup the callback event handler
            UploadDataCompletedEventHandler? handler = null;
            handler = (sender, e) => HandleCompletion(tcs, e, (args) => args.Result, handler, (webClient, completion) => webClient.UploadDataCompleted -= completion);
            UploadDataCompleted += handler;

            // Start the async operation.
            try { UploadDataAsync(address, method, data, tcs); }
            catch
            {
                UploadDataCompleted -= handler;
                throw;
            }

            // Return the task that represents the async operation
            return tcs.Task;
        }

        public Task<byte[]> UploadFileTaskAsync(string address, string fileName) =>
            UploadFileTaskAsync(GetUri(address), null, fileName);

        public Task<byte[]> UploadFileTaskAsync(Uri address, string fileName) =>
            UploadFileTaskAsync(address, null, fileName);

        public Task<byte[]> UploadFileTaskAsync(string address, string? method, string fileName) =>
            UploadFileTaskAsync(GetUri(address), method, fileName);

        public Task<byte[]> UploadFileTaskAsync(Uri address, string? method, string fileName)
        {
            // Create the task to be returned
            var tcs = new TaskCompletionSource<byte[]>(address);

            // Setup the callback event handler
            UploadFileCompletedEventHandler? handler = null;
            handler = (sender, e) => HandleCompletion(tcs, e, (args) => args.Result, handler, (webClient, completion) => webClient.UploadFileCompleted -= completion);
            UploadFileCompleted += handler;

            // Start the async operation.
            try { UploadFileAsync(address, method, fileName, tcs); }
            catch
            {
                UploadFileCompleted -= handler;
                throw;
            }

            // Return the task that represents the async operation
            return tcs.Task;
        }

        public Task<byte[]> UploadValuesTaskAsync(string address, NameValueCollection data) =>
            UploadValuesTaskAsync(GetUri(address), null, data);

        public Task<byte[]> UploadValuesTaskAsync(string address, string? method, NameValueCollection data) =>
            UploadValuesTaskAsync(GetUri(address), method, data);

        public Task<byte[]> UploadValuesTaskAsync(Uri address, NameValueCollection data) =>
            UploadValuesTaskAsync(address, null, data);

        public Task<byte[]> UploadValuesTaskAsync(Uri address, string? method, NameValueCollection data)
        {
            // Create the task to be returned
            var tcs = new TaskCompletionSource<byte[]>(address);

            // Setup the callback event handler
            UploadValuesCompletedEventHandler? handler = null;
            handler = (sender, e) => HandleCompletion(tcs, e, (args) => args.Result, handler, (webClient, completion) => webClient.UploadValuesCompleted -= completion);
            UploadValuesCompleted += handler;

            // Start the async operation.
            try { UploadValuesAsync(address, method, data, tcs); }
            catch
            {
                UploadValuesCompleted -= handler;
                throw;
            }

            // Return the task that represents the async operation
            return tcs.Task;
        }

        private void HandleCompletion<TAsyncCompletedEventArgs, TCompletionDelegate, T>(TaskCompletionSource<T> tcs, TAsyncCompletedEventArgs e, Func<TAsyncCompletedEventArgs, T> getResult, TCompletionDelegate handler, Action<WebClient, TCompletionDelegate> unregisterHandler)
            where TAsyncCompletedEventArgs : AsyncCompletedEventArgs
        {
            if (e.UserState == tcs)
            {
                try { unregisterHandler(this, handler); }
                finally
                {
                    if (e.Error != null) tcs.TrySetException(e.Error);
                    else if (e.Cancelled) tcs.TrySetCanceled();
                    else tcs.TrySetResult(getResult(e));
                }
            }
        }

        private void PostProgressChanged(AsyncOperation asyncOp, ProgressData progress)
        {
            if (asyncOp != null && (progress.BytesSent > 0 || progress.BytesReceived > 0))
            {
                int progressPercentage;
                if (progress.HasUploadPhase)
                {
                    if (UploadProgressChanged != null)
                    {
                        progressPercentage = progress.TotalBytesToReceive < 0 && progress.BytesReceived == 0 ?
                            progress.TotalBytesToSend < 0 ? 0 : progress.TotalBytesToSend == 0 ? 50 : (int)((50 * progress.BytesSent) / progress.TotalBytesToSend) :
                            progress.TotalBytesToSend < 0 ? 50 : progress.TotalBytesToReceive == 0 ? 100 : (int)((50 * progress.BytesReceived) / progress.TotalBytesToReceive + 50);
                        asyncOp.Post(_reportUploadProgressChanged!, new UploadProgressChangedEventArgs(progressPercentage, asyncOp.UserSuppliedState!, progress.BytesSent, progress.TotalBytesToSend, progress.BytesReceived, progress.TotalBytesToReceive));
                    }
                }
                else if (DownloadProgressChanged != null)
                {
                    progressPercentage = progress.TotalBytesToReceive < 0 ? 0 : progress.TotalBytesToReceive == 0 ? 100 : (int)((100 * progress.BytesReceived) / progress.TotalBytesToReceive);
                    asyncOp.Post(_reportDownloadProgressChanged!, new DownloadProgressChangedEventArgs(progressPercentage, asyncOp.UserSuppliedState!, progress.BytesReceived, progress.TotalBytesToReceive));
                }
            }
        }

        #region Supporting Types
        private sealed class ProgressData
        {
            internal long BytesSent;
            internal long TotalBytesToSend = -1;
            internal long BytesReceived;
            internal long TotalBytesToReceive = -1;
            internal bool HasUploadPhase;

            internal void Reset()
            {
                BytesSent = 0;
                TotalBytesToSend = -1;
                BytesReceived = 0;
                TotalBytesToReceive = -1;
                HasUploadPhase = false;
            }
        }

        private sealed class WebClientWriteStream : DelegatingStream
        {
            private readonly WebRequest _request;
            private readonly WebClient _webClient;

            public WebClientWriteStream(Stream stream, WebRequest request, WebClient webClient) : base(stream)
            {
                _request = request;
                _webClient = webClient;
            }

            protected override void Dispose(bool disposing)
            {
                try
                {
                    if (disposing)
                    {
                        _webClient.GetWebResponse(_request).Dispose();
                    }
                }
                finally
                {
                    base.Dispose(disposing);
                }
            }
        }
        #endregion

        #region Obsolete designer support
        //introduced to support design-time loading of System.Windows.dll

        [Obsolete("This API supports the .NET Framework infrastructure and is not intended to be used directly from your code.", true)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        public bool AllowReadStreamBuffering { get; set; }

        [Obsolete("This API supports the .NET Framework infrastructure and is not intended to be used directly from your code.", true)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        public bool AllowWriteStreamBuffering { get; set; }

        [Obsolete("This API supports the .NET Framework infrastructure and is not intended to be used directly from your code.", true)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        public event WriteStreamClosedEventHandler? WriteStreamClosed { add { } remove { } }

        [Obsolete("This API supports the .NET Framework infrastructure and is not intended to be used directly from your code.", true)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        protected virtual void OnWriteStreamClosed(WriteStreamClosedEventArgs e) { }
        #endregion
    }

    #region Delegates and supporting *CompletedEventArgs classes used by event-based async code
    public delegate void OpenReadCompletedEventHandler(object sender, OpenReadCompletedEventArgs e);
    public delegate void OpenWriteCompletedEventHandler(object sender, OpenWriteCompletedEventArgs e);
    public delegate void DownloadStringCompletedEventHandler(object sender, DownloadStringCompletedEventArgs e);
    public delegate void DownloadDataCompletedEventHandler(object sender, DownloadDataCompletedEventArgs e);
    public delegate void UploadStringCompletedEventHandler(object sender, UploadStringCompletedEventArgs e);
    public delegate void UploadDataCompletedEventHandler(object sender, UploadDataCompletedEventArgs e);
    public delegate void UploadFileCompletedEventHandler(object sender, UploadFileCompletedEventArgs e);
    public delegate void UploadValuesCompletedEventHandler(object sender, UploadValuesCompletedEventArgs e);
    public delegate void DownloadProgressChangedEventHandler(object sender, DownloadProgressChangedEventArgs e);
    public delegate void UploadProgressChangedEventHandler(object sender, UploadProgressChangedEventArgs e);
    [EditorBrowsable(EditorBrowsableState.Never)]
    public delegate void WriteStreamClosedEventHandler(object sender, WriteStreamClosedEventArgs e);

    public class OpenReadCompletedEventArgs : AsyncCompletedEventArgs
    {
        private readonly Stream? _result;

        internal OpenReadCompletedEventArgs(Stream? result, Exception? exception, bool cancelled, object? userToken) : base(exception, cancelled, userToken)
        {
            _result = result;
        }

        public Stream Result
        {
            get
            {
                RaiseExceptionIfNecessary();
                return _result!;
            }
        }
    }

    public class OpenWriteCompletedEventArgs : AsyncCompletedEventArgs
    {
        private readonly Stream? _result;

        internal OpenWriteCompletedEventArgs(Stream? result, Exception? exception, bool cancelled, object? userToken) : base(exception, cancelled, userToken)
        {
            _result = result;
        }

        public Stream Result
        {
            get
            {
                RaiseExceptionIfNecessary();
                return _result!;
            }
        }
    }

    public class DownloadStringCompletedEventArgs : AsyncCompletedEventArgs
    {
        private readonly string? _result;

        internal DownloadStringCompletedEventArgs(string? result, Exception? exception, bool cancelled, object? userToken) : base(exception, cancelled, userToken)
        {
            _result = result;
        }

        public string Result
        {
            get
            {
                RaiseExceptionIfNecessary();
                return _result!;
            }
        }

    }

    public class DownloadDataCompletedEventArgs : AsyncCompletedEventArgs
    {
        private readonly byte[]? _result;

        internal DownloadDataCompletedEventArgs(byte[]? result, Exception? exception, bool cancelled, object? userToken) : base(exception, cancelled, userToken)
        {
            _result = result;
        }

        public byte[] Result
        {
            get
            {
                RaiseExceptionIfNecessary();
                return _result!;
            }
        }
    }

    public class UploadStringCompletedEventArgs : AsyncCompletedEventArgs
    {
        private readonly string? _result;

        internal UploadStringCompletedEventArgs(string? result, Exception? exception, bool cancelled, object? userToken) : base(exception, cancelled, userToken)
        {
            _result = result;
        }

        public string Result
        {
            get
            {
                RaiseExceptionIfNecessary();
                return _result!;
            }
        }
    }

    public class UploadDataCompletedEventArgs : AsyncCompletedEventArgs
    {
        private readonly byte[]? _result;

        internal UploadDataCompletedEventArgs(byte[]? result, Exception? exception, bool cancelled, object? userToken) : base(exception, cancelled, userToken)
        {
            _result = result;
        }

        public byte[] Result
        {
            get
            {
                RaiseExceptionIfNecessary();
                return _result!;
            }
        }
    }

    public class UploadFileCompletedEventArgs : AsyncCompletedEventArgs
    {
        private readonly byte[]? _result;

        internal UploadFileCompletedEventArgs(byte[]? result, Exception? exception, bool cancelled, object? userToken) : base(exception, cancelled, userToken)
        {
            _result = result;
        }

        public byte[] Result
        {
            get
            {
                RaiseExceptionIfNecessary();
                return _result!;
            }
        }
    }

    public class UploadValuesCompletedEventArgs : AsyncCompletedEventArgs
    {
        private readonly byte[]? _result;

        internal UploadValuesCompletedEventArgs(byte[]? result, Exception? exception, bool cancelled, object? userToken) : base(exception, cancelled, userToken)
        {
            _result = result;
        }

        public byte[] Result
        {
            get
            {
                RaiseExceptionIfNecessary();
                return _result!;
            }
        }
    }

    public class DownloadProgressChangedEventArgs : ProgressChangedEventArgs
    {
        internal DownloadProgressChangedEventArgs(int progressPercentage, object? userToken, long bytesReceived, long totalBytesToReceive) :
            base(progressPercentage, userToken)
        {
            BytesReceived = bytesReceived;
            TotalBytesToReceive = totalBytesToReceive;
        }

        public long BytesReceived { get; }
        public long TotalBytesToReceive { get; }
    }


    public class UploadProgressChangedEventArgs : ProgressChangedEventArgs
    {
        internal UploadProgressChangedEventArgs(int progressPercentage, object? userToken, long bytesSent, long totalBytesToSend, long bytesReceived, long totalBytesToReceive) :
            base(progressPercentage, userToken)
        {
            BytesReceived = bytesReceived;
            TotalBytesToReceive = totalBytesToReceive;
            BytesSent = bytesSent;
            TotalBytesToSend = totalBytesToSend;
        }

        public long BytesReceived { get; }
        public long TotalBytesToReceive { get; }
        public long BytesSent { get; }
        public long TotalBytesToSend { get; }
    }


    [EditorBrowsable(EditorBrowsableState.Never)]
    public class WriteStreamClosedEventArgs : EventArgs
    {
        [Obsolete("This API supports the .NET Framework infrastructure and is not intended to be used directly from your code.", true)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        public WriteStreamClosedEventArgs() { }

        [Obsolete("This API supports the .NET Framework infrastructure and is not intended to be used directly from your code.", true)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        public Exception? Error { get { return null; } }
    }
    #endregion
}
